/* eslint-disable no-bitwise, no-mixed-operators, class-methods-use-this, camelcase */
const {BigInteger} = require('./jsbn')
const _            = require('./utils')

const copyArray = function (sourceArray, sourceIndex, destinationArray, destinationIndex, length) {
    for (let i = 0; i < length; i++) {
        destinationArray[destinationIndex + i] = sourceArray[sourceIndex + i]
    }
}

const Int32 = {
    minValue: -0b10000000000000000000000000000000,
    maxValue: 0b1111111111111111111111111111111,
    parse(n) {
        if (n < this.minValue) {
            const bigInteger         = Number(-n)
            const bigIntegerRadix    = bigInteger.toString(2)
            const subBigIntegerRadix = bigIntegerRadix.substr(bigIntegerRadix.length - 31, 31)
            let reBigIntegerRadix    = ''
            for (let i = 0; i < subBigIntegerRadix.length; i++) {
                const subBigIntegerRadixItem = subBigIntegerRadix.substr(i, 1)
                reBigIntegerRadix += subBigIntegerRadixItem === '0' ? '1' : '0'
            }
            const result = parseInt(reBigIntegerRadix, 2)
            return (result + 1)
        } else if (n > this.maxValue) {
            const bigInteger         = Number(n)
            const bigIntegerRadix    = bigInteger.toString(2)
            const subBigIntegerRadix = bigIntegerRadix.substr(bigIntegerRadix.length - 31, 31)
            let reBigIntegerRadix    = ''
            for (let i = 0; i < subBigIntegerRadix.length; i++) {
                const subBigIntegerRadixItem = subBigIntegerRadix.substr(i, 1)
                reBigIntegerRadix += subBigIntegerRadixItem === '0' ? '1' : '0'
            }
            const result = parseInt(reBigIntegerRadix, 2)
            return -(result + 1)
        } else {
            return n
        }
    },
    parseByte(n) {
        if (n < 0) {
            const bigInteger         = Number(-n)
            const bigIntegerRadix    = bigInteger.toString(2)
            const subBigIntegerRadix = bigIntegerRadix.substr(bigIntegerRadix.length - 8, 8)
            let reBigIntegerRadix    = ''
            for (let i = 0; i < subBigIntegerRadix.length; i++) {
                const subBigIntegerRadixItem = subBigIntegerRadix.substr(i, 1)
                reBigIntegerRadix += subBigIntegerRadixItem === '0' ? '1' : '0'
            }
            const result = parseInt(reBigIntegerRadix, 2)
            return (result + 1) % 256
        } else if (n > 255) {
            const bigInteger      = Number(n)
            const bigIntegerRadix = bigInteger.toString(2)
            return parseInt(bigIntegerRadix.substr(bigIntegerRadix.length - 8, 8), 2)
        } else {
            return n
        }
    }
}

class SM3Digest {
    constructor(...args) {
        this.xBuf          = []
        this.xBufOff       = 0
        this.byteCount     = 0
        this.DIGEST_LENGTH = 32
        this.v0            = [0x7380166f, 0x4914b2b9, 0x172442d7, 0xda8a0600,
            0xa96f30bc, 0x163138aa, 0xe38dee4d, 0xb0fb0e4e]
        this.v0            = [0x7380166f, 0x4914b2b9, 0x172442d7, -628488704,
            -1452330820, 0x163138aa, -477237683, -1325724082]
        this.v             = new Array(8)
        this.v_            = new Array(8)
        this.X0            = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
        this.X             = new Array(68)
        this.xOff          = 0
        this.T_00_15       = 0x79cc4519
        this.T_16_63       = 0x7a879d8a
        if (args.length > 0) {
            this.initDigest(args[0])
        } else {
            this.init()
        }
    }

    init() {
        this.xBuf = new Array(4)
        this.reset()
    }

    initDigest(t) {
        this.xBuf      = [].concat(t.xBuf)
        this.xBufOff   = t.xBufOff
        this.byteCount = t.byteCount
        copyArray(t.X, 0, this.X, 0, t.X.length)
        this.xOff = t.xOff
        copyArray(t.v, 0, this.v, 0, t.v.length)
    }

    getDigestSize() {
        return this.DIGEST_LENGTH
    }

    reset() {
        this.byteCount = 0
        this.xBufOff   = 0

        const keys = Object.keys(this.xBuf)
        for (let i = 0, len = keys.length; i < len; i++) {
            this.xBuf[keys[i]] = null
        }

        copyArray(this.v0, 0, this.v, 0, this.v0.length)
        this.xOff = 0
        copyArray(this.X0, 0, this.X, 0, this.X0.length)
    }

    processBlock() {
        let i
        const ww  = this.X
        const ww_ = new Array(64)
        for (i = 16; i < 68; i++) {
            ww[i] = this.p1(ww[i - 16] ^ ww[i - 9] ^ (this.rotate(ww[i - 3], 15))) ^ (this.rotate(ww[i - 13], 7)) ^ ww[i - 6]
        }
        for (i = 0; i < 64; i++) {
            ww_[i] = ww[i] ^ ww[i + 4]
        }
        const vv  = this.v
        const vv_ = this.v_
        copyArray(vv, 0, vv_, 0, this.v0.length)
        let SS1
        let SS2
        let TT1
        let TT2
        let aaa
        for (i = 0; i < 16; i++) {
            aaa    = this.rotate(vv_[0], 12)
            SS1    = Int32.parse(Int32.parse(aaa + vv_[4]) + this.rotate(this.T_00_15, i))
            SS1    = this.rotate(SS1, 7)
            SS2    = SS1 ^ aaa
            TT1    = Int32.parse(Int32.parse(this.ff_00_15(vv_[0], vv_[1], vv_[2]) + vv_[3]) + SS2) + ww_[i]
            TT2    = Int32.parse(Int32.parse(this.gg_00_15(vv_[4], vv_[5], vv_[6]) + vv_[7]) + SS1) + ww[i]
            vv_[3] = vv_[2]
            vv_[2] = this.rotate(vv_[1], 9)
            vv_[1] = vv_[0]
            vv_[0] = TT1
            vv_[7] = vv_[6]
            vv_[6] = this.rotate(vv_[5], 19)
            vv_[5] = vv_[4]
            vv_[4] = this.p0(TT2)
        }
        for (i = 16; i < 64; i++) {
            aaa    = this.rotate(vv_[0], 12)
            SS1    = Int32.parse(Int32.parse(aaa + vv_[4]) + this.rotate(this.T_16_63, i))
            SS1    = this.rotate(SS1, 7)
            SS2    = SS1 ^ aaa
            TT1    = Int32.parse(Int32.parse(this.ff_16_63(vv_[0], vv_[1], vv_[2]) + vv_[3]) + SS2) + ww_[i]
            TT2    = Int32.parse(Int32.parse(this.gg_16_63(vv_[4], vv_[5], vv_[6]) + vv_[7]) + SS1) + ww[i]
            vv_[3] = vv_[2]
            vv_[2] = this.rotate(vv_[1], 9)
            vv_[1] = vv_[0]
            vv_[0] = TT1
            vv_[7] = vv_[6]
            vv_[6] = this.rotate(vv_[5], 19)
            vv_[5] = vv_[4]
            vv_[4] = this.p0(TT2)
        }
        for (i = 0; i < 8; i++) {
            vv[i] ^= Int32.parse(vv_[i])
        }
        this.xOff = 0
        copyArray(this.X0, 0, this.X, 0, this.X0.length)
    }

    processWord(in_Renamed, inOff) {
        let n             = in_Renamed[inOff] << 24
        n |= (in_Renamed[++inOff] & 0xff) << 16
        n |= (in_Renamed[++inOff] & 0xff) << 8
        n |= (in_Renamed[++inOff] & 0xff)
        this.X[this.xOff] = n
        if (++this.xOff === 16) {
            this.processBlock()
        }
    }

    processLength(bitLength) {
        if (this.xOff > 14) {
            this.processBlock()
        }
        this.X[14] = (this.urShiftLong(bitLength, 32))
        this.X[15] = (bitLength & (0xffffffff))
    }

    intToBigEndian(n, bs, off) {
        bs[off]   = Int32.parseByte(this.urShift(n, 24)) & 0xff
        bs[++off] = Int32.parseByte(this.urShift(n, 16)) & 0xff
        bs[++off] = Int32.parseByte(this.urShift(n, 8)) & 0xff
        bs[++off] = Int32.parseByte(n) & 0xff
    }

    doFinal(out_Renamed, outOff) {
        this.finish()
        for (let i = 0; i < 8; i++) {
            this.intToBigEndian(this.v[i], out_Renamed, outOff + i * 4)
        }
        this.reset()
        return this.DIGEST_LENGTH
    }

    update(input) {
        this.xBuf[this.xBufOff++] = input
        if (this.xBufOff === this.xBuf.length) {
            this.processWord(this.xBuf, 0)
            this.xBufOff = 0
        }
        this.byteCount++
    }

    blockUpdate(input, inOff, length) {
        while ((this.xBufOff !== 0) && (length > 0)) {
            this.update(input[inOff])
            inOff++
            length--
        }
        while (length > this.xBuf.length) {
            this.processWord(input, inOff)
            inOff += this.xBuf.length
            length -= this.xBuf.length
            this.byteCount += this.xBuf.length
        }
        while (length > 0) {
            this.update(input[inOff])
            inOff++
            length--
        }
    }

    finish() {
        const bitLength = (this.byteCount << 3)
        this.update((128))
        while (this.xBufOff !== 0) this.update((0))
        this.processLength(bitLength)
        this.processBlock()
    }

    rotate(x, n) {
        return (x << n) | (this.urShift(x, (32 - n)))
    }

    p0(X) {
        return ((X) ^ this.rotate((X), 9) ^ this.rotate((X), 17))
    }

    p1(X) {
        return ((X) ^ this.rotate((X), 15) ^ this.rotate((X), 23))
    }

    ff_00_15(X, Y, Z) {
        return (X ^ Y ^ Z)
    }

    ff_16_63(X, Y, Z) {
        return ((X & Y) | (X & Z) | (Y & Z))
    }

    gg_00_15(X, Y, Z) {
        return (X ^ Y ^ Z)
    }

    gg_16_63(X, Y, Z) {
        return ((X & Y) | (~X & Z))
    }

    urShift(number, bits) {
        if (number > Int32.maxValue || number < Int32.minValue) {
            number = Int32.parse(number)
        }
        return number >>> bits
    }

    urShiftLong(number, bits) {
        let returnV
        const big = new BigInteger()
        big.fromInt(number)
        if (big.signum() >= 0) {
            returnV = big.shiftRight(bits).intValue()
        } else {
            const bigAdd = new BigInteger()
            bigAdd.fromInt(2)
            const shiftLeftBits = ~bits
            let shiftLeftNumber = ''
            if (shiftLeftBits < 0) {
                const shiftRightBits = 64 + shiftLeftBits
                for (let i = 0; i < shiftRightBits; i++) {
                    shiftLeftNumber += '0'
                }
                const shiftLeftNumberBigAdd = new BigInteger()
                shiftLeftNumberBigAdd.fromInt(number >> bits)
                const shiftLeftNumberBig = new BigInteger('10' + shiftLeftNumber, 2)
                shiftLeftNumber          = shiftLeftNumberBig.toRadix(10)
                const r                  = shiftLeftNumberBig.add(shiftLeftNumberBigAdd)
                returnV                  = r.toRadix(10)
            } else {
                shiftLeftNumber = bigAdd.shiftLeft((~bits)).intValue()
                returnV         = (number >> bits) + shiftLeftNumber
            }
        }
        return returnV
    }

    getZ(g, publicKey, userId) {
        // ZA=H256(ENTLA ∥ IDA ∥ a ∥ b ∥ xG ∥ yG ∥xA ∥yA)
        let len = 0
        if (userId) {
            if (typeof userId !== 'string') throw new Error(`sm2: Type of userId Must be String! Receive Type: ${typeof userId}`)
            if (userId.length >= 8192) throw new Error(`sm2: The Length of userId Must Less Than 8192! Length: ${userId.length}`)

            userId = _.parseUtf8StringToHex(userId)
            len    = userId.length * 4
        }
        this.update((len >> 8 & 0x00ff))
        this.update((len & 0x00ff))
        if (userId) {
            const userIdWords = _.hexToArray(userId)
            this.blockUpdate(userIdWords, 0, userIdWords.length)
        }
        const aWords  = _.hexToArray(_.leftPad(g.curve.a.toBigInteger().toRadix(16), 64))
        const bWords  = _.hexToArray(_.leftPad(g.curve.b.toBigInteger().toRadix(16), 64))
        const gxWords = _.hexToArray(_.leftPad(g.getX().toBigInteger().toRadix(16), 64))
        const gyWords = _.hexToArray(_.leftPad(g.getY().toBigInteger().toRadix(16), 64))
        const pxWords = _.hexToArray(publicKey.substr(0, 64))
        const pyWords = _.hexToArray(publicKey.substr(64, 64))
        this.blockUpdate(aWords, 0, aWords.length)
        this.blockUpdate(bWords, 0, bWords.length)
        this.blockUpdate(gxWords, 0, gxWords.length)
        this.blockUpdate(gyWords, 0, gyWords.length)
        this.blockUpdate(pxWords, 0, pxWords.length)
        this.blockUpdate(pyWords, 0, pyWords.length)
        const md = new Array(this.getDigestSize())
        this.doFinal(md, 0)
        return md
    }
}

module.exports = SM3Digest
